Skip to content

Research: segmented, resumable reloads (proof of concept) - #2251

Draft
sunzenshen wants to merge 67 commits into
NeotokyoRebuild:masterfrom
sunzenshen:research-reload-phases
Draft

sunzenshen wants to merge 67 commits into
NeotokyoRebuild:masterfrom
sunzenshen:research-reload-phases

Conversation

@sunzenshen

@sunzenshen sunzenshen commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Draft. One branch that combines every reload idea of proposal 0057 so reviewers can see them together and say
which parts to split into their own PRs. It is also the bundle of all our pending fixes: Option A #2244
(player animation on the SDK base anim state), #2245, #2248 (with its slide fixes), #2249, #2246, #2250, and the
Tachi reloadempty asset (NeotokyoRebuild/neoAssets#170), so one build previews them all together. It needs the
matching neoAssets branch (research-reload-phases), which carries the recompiled viewmodels and the sound pieces.

Policy: whenever one of the bundled PRs merges, this branch is rebased onto master, the merged commits drop out and
any changed assumption (a sequence name, an activity, a hook) is resolved here, so the branch shrinks to what is not
yet merged.

What it does

  • A reload is a sequence of phases: drop (magazine out), seat (fresh magazine in), slap (where the gun has a tap or
    slap), bolt (dry reloads only). Each phase counts at its point in the animation, listed per sequence in the weapon
    script (ReloadPhases block, generated from a table so assets and script agree). The drop leaves only a
    chambered round, the seat loads one magazine from the reserve, the bolt chambers a round.
  • Partial reloads: with a round chambered the gun plays a shorter reload_partial (no bolt) and ends at
    magazine + 1 (30 + 1 = 31); a dry reload ends at 30. The wait to fire is the length of the animation that plays.
  • Resumable reloads: a switch keeps the phases already counted; switching back resumes at the next phase, with
    the hand reaching from the hold to that phase's grab pose. After the drop the resume starts in the draw itself
    (draw_resume_drop_*): the gun comes up one-handed with the support hand and fresh magazine out of view, then swoops
    in. Always on, no option.
  • Sound in per-phase pieces: each reload wav is cut at the quiet gaps into drop / seat / slap / bolt pieces, each
    played at its own time (several moved to meet the animation, below); a switch stops them; resumed phases do not
    replay finished pieces.
  • Empty pistols play reloadempty with the slide release (the Milso's shipped one, a built one for the Tachi); a
    pistol switched away after the seat resumes with the slide release alone. The Kyla has no chamber: its reload
    keeps the live rounds and fills only the empty slots, as today (reload at 4: 6, reserve -2).
  • Supa7: the tube is a stack of shells and slugs in load order, the chamber is tracked (shell, slug, fired hull),
    a rack ejects the chamber and feeds the top item; the shell insert plays at 1.47x so each one finishes within the
    unchanged 0.5 s; a reload press with the tube full racks once (rack sequence).
  • HUD: the magazine row counts the chambered round (31/31 after a partial); with the row overflowing into a +
    mark, the mark is filled only when the gun is full.
  • Third person: other players (and spectators in chase or in-eye) see the reload gesture for phase reloads and
    draw-resumes. A resumed reload starts the gesture where the resume starts (the credit time of the last finished
    phase over the full reload's length, sent in the event's data), and the gesture ends with the reload: at its end,
    on a switch, a drop or death (PLAYERANIMEVENT_RELOAD_END), so a cancelled reload stops on the body too.
  • Guns without a phase block (SMAC, grenades) and every gun's normal reload keep working as before.

What the bundle adds beyond the bundled PRs

  • The reload gesture hooks on Option A's anim state: Animate players on the SDK base player anim state #2244 deletes hl2mp_playeranimstate.*, so the third-person
    hooks above live in CNEOPlayerAnimState: PLAYERANIMEVENT_RELOAD_END, the resumed reload's start cycle from the
    event data, and the end of the reload gesture (in the Animate players on the SDK base player anim state #2244 merge, a75be2a).
  • A 0.2 s fade for a reload gesture that ends early or is switched away (0d8e2b6, 4788a5e): Option A ends a
    gesture at once, so a partial reload's early end, or a switch mid-reload, snapped the third-person gun from the
    reload pose to the aim pose in one frame; it now fades out over 0.2 s as the HL2MP gesture slots did. On a switch
    this deliberately differs from Animate players on the SDK base player anim state #2244, which stops every gesture at once on a weapon change (a fire or throw gesture
    still does here).
  • The pistol slide check (b7b8a08): a dry Milso or Tachi reload interrupted after the seat has rounds in the
    magazine but none chambered, with the slide still back. The draw and the idle now use their empty sequences for
    that state too (drawempty, idle01empty), so the slide no longer goes forward on the draw and snaps back when the
    slide release resumes. Pistols only.
  • The Tachi's muzzle-flash events (neoAssets): the research decompile dropped the shipped AE_MUZZLEFLASH on the
    Tachi's fire sequences; the recompiled Tachi has them again, as in the neoAssets Tachi reloadempty PR.

Commits, grouped for splitting

  1. Base, merged as is: Finish reloads at the last reload click #2245 reload-ready-at-cue (ba69cc1), vm-empty-draw incl. vm-empty-slide-lock
    (63733b3, and its slide fixes in 0dd0c46), fix-pistol-sprint-reload (9f99ae2), Keep the muzzle flash viewmodel on map cleanup #2246
    fix-muzzleflash-class-change (c5e4349), Null-check the objective carrier in ghost boundary #2250 fix-ghost-boundary-null-player (9a06a56), Animate players on the SDK base player anim state #2244
    anim-sdk-base (a75be2a, with the gesture hooks ported).

  2. Phase machine: 5058940 Parse the ReloadPhases weapon-script block; 5f5fc36 Track reload phases and credit ammo
    per phase; 4a54e94 Select partial and resume reload sequences by name (with the draw-resume in Deploy).

  3. Sound pieces: a7a5ac2, 8d7daa2 Play the reload sound in per-phase pieces; ef49e0f Allow a second sound piece
    per reload phase; 35b2bf3 Play a phase's sound pieces after its credit too.

  4. HUD: 545bb3e Show the chambered round on the ammo HUD; d4df589 Fill the HUD's overflow mark for a full gun.

  5. Pistols: 5f64639 Reload an empty pistol with its slide release; 38ce827 placeholder slide-release sound;
    faa65c6 Top up a revolver's cylinder instead of emptying it.

  6. Per-gun data (weapon scripts and sound entries only): ZR68S 9d3c2af 39ec946 46c405c, ZR68C 10c0013, ZR68L
    0e37c6f, MX 897e3e1, M41 146289f, Jitte eda7000 12b1de8, SRM / MPN f24237a, AA13 / PZ 6f66303, SRS
    fdaef0b, Milso / Tachi c12716e, Kyla 402163a.

  7. Supa7: bde9bf4 tube, chamber and pump as state; cee96d3 insert at the shell pace; 165e63b slug insert at its
    own pace; 9323316 rack on a reload press with the tube full.

  8. Third person: 5d3644b Show the reload gesture for phase reloads (also stops the sound pieces on a drop and on
    death); 74cac6b Start the reload gesture where a resumed reload resumes; 6040298 Look up reload sequences on
    the viewmodel (the client predicted reload for a partial and 0 for the resume start, because the weapon's own
    model on the client is the world model).

  9. Bundle: b7b8a08 Draw and idle a pistol with its slide back until the release; 0d8e2b6 Fade out a reload
    gesture that ends early; 4788a5e Fade out a reload gesture on a weapon switch.

Splits that stand alone: group 4 (HUD) needs 2; group 5 needs 2; group 7 is independent of 2-6.

Per-gun timings (animation length = wait to fire)

gun dry reload partial reload resume after the drop (draw-resume) resume after the seat (dry / partial) resume after the slap (dry)
ZR68S 2.70 s 2.20 s 2.80 s / 2.30 s 1.77 s / 1.27 s 1.67 s
ZR68C 2.70 s 2.20 s 2.80 s / 2.30 s 1.77 s / 1.27 s 1.67 s
ZR68L 2.70 s 2.20 s 2.90 s / 2.40 s 1.80 s / 1.30 s 1.67 s
MX, MX-S 2.87 s 2.10 s 2.97 s / 2.20 s 1.77 s / 1.00 s 1.67 s
M41, M41S, M41L 3.70 s 2.77 s 3.93 s / 3.00 s 2.03 s / 1.17 s 1.20 s
Jitte, Jitte scoped 3.30 s 2.57 s 2.77 s / 2.03 s 1.27 s / - -
SRM, SRM-S 3.07 s 2.37 s 2.93 s / 2.23 s 1.17 s / - -
MPN, MPN unsilenced 3.20 s 2.07 s 3.63 s / 2.50 s 1.47 s / - -
AA13 4.67 s 3.80 s 3.87 s / 3.00 s 1.40 s / - -
PZ 5.77 s 5.00 s 4.73 s / 3.97 s 1.83 s / 1.07 s 1.40 s
SRS 3.70 s 2.43 s 3.40 s / 2.13 s 1.60 s / - -
Milso 2.10 s 2.10 s 2.43 s / 2.43 s 0.50 s / - -
Tachi 2.10 s 2.10 s 2.57 s / 2.57 s 0.50 s / - -
Kyla 2.37 s = dry (no chamber) 2.67 s / - - / - -
Supa7 per shell (0.5 s each, insert played at 1.47x) - - - standalone rack 1.57 s

Resumes after the drop through the draw include the draw (0.8 s on most guns), so they replace draw + reload.

Tests

  • Credits, switches and resumes on every permutation of every gun, filmed in game with sound (listen server,
    neo_reloadlog): every take played the planned sequence and ended at the expected count.

  • Client prediction (ZR68S): every credit predicted on the client at the same time with the same count.

  • Debug, 12 bots, 1 round: ntre_dawn_ctg, ntre_threadplate_ctg, ntre_isolation_ctg, ntre_bullet_tdm: no asserts.

  • Bot census (8 spawns on ntre_threadplate_ctg, every class and XP tier): 16 guns reloaded through the phases (MX 72
    reload sequences, ZR68S 43, Kyla 40, ...), resumes in normal play, every reload ended or was cancelled by a switch.

  • Third person (ZR68S, listen server, thirdperson): gesture start on the server and the owner's client for every
    phase sequence, with start data 0 (fresh, draw-resume), 0.493 (after the seat), 0.629 (after the slap); the end at
    the sequence end and at each switch. Death mid-reload (kill at the slap): gesture end at the death, no bolt piece
    afterwards. Spectating bots (ntre_dawn_ctg, chase and in-eye): the spectator received each start and end the server
    sent for the watched bot. Bot census (4 spawns, threadplate): gesture starts = phase starts in every match.

  • Bundle build (all of the above merged): Milso and Tachi permutations re-filmed with sound; the resume after the
    seat on a dry pistol now plays drawempty -> reload_from_seat_dry -> idle01, and the slide-release piece plays.
    Third person on Option A (ZR68S): resumes start at cycle 0.496 (after the seat) and 0.632 (after the slap); a
    partial ends early at ~0.92 and fades over 0.2 s, and a switch mid-reload fades over 0.2 s too. Debug, 12 bots, 1 round on the same four maps: no asserts. Bot
    census (8 spawns, threadplate, Release): 8/8 clean, no reload stalls (Tachi 32 reloadempty, Milso 26, two
    slide-release resumes each).

Open items

  • Option A's own cases (jump pose, upper-body blend, sprint legs, feet squaring up) were not re-filmed on the bundle
    build; Animate players on the SDK base player anim state #2244's clips stand for them.

  • Pistol slide-release sound is a placeholder (a slice of the game's MP5 slide sound); needs a proper recording.

  • Holstering after an interruption past the drop briefly fades the magazine back into the gun as it lowers (0.3 s).

  • Supa7 shell-shot pump sound: parked; the same in classic.

  • Stealth option: some players may not want a resume (and its sound) started for them on switch-back; a later option
    beside cl_autoreload_when_empty.

  • Behavior changes to decide: loading a Supa7 slug with a shell chambered racks the shell out (lost); the HUD reads a 30-round gun as not full (31 is full).

  • Supa7 per-shell shell/slug glyphs on the HUD (the chamber icon already shows which is chambered).

Decisions and where they come from

  • Phase model, chamber tracking (+1 on a partial), full magazines only, forced slap phase, partial = after the last
    magazine contact then straight to the hold, resume = reach to the grab pose: user, 2026-10-15 (proposal 0057
    research, sections 7-9).
  • Auto-resume always on, no option: user, 2026-10-15.
  • Resume after the drop starts in the draw, no magazine slide, no bodygroup: user, 2026-10-15 evening.
  • Reach length sized so the hand peaks at the shipped 2.5 units per frame: review of the first builds, 2026-10-15.
  • Supa7: slug racks a chambered shell out (realistic), standalone rack on a full tube, empty tube refused, insert at
    1.47x (classic restarts every 0.5 s too), shell-shot pump sound parked: user, 2026-10-15.
  • Kyla tops up its cylinder (keeps live rounds, fills the empty slots); the full-magazine rule is for magazine guns:
    user, 2026-10-15 evening.
  • Fire time never shorter than the animation that plays: reviewer on Finish reloads at the last reload click #2245.

Videos

Per gun, every permutation (dry, partial, resume after the drop / seat / slap) with sound and HUD:
zr68s, zr68c, zr68l, mx, mx_silenced, m41, m41s, m41l, jitte, jittescoped, srm, srm_s,
mpn, mpn_unsilenced, aa13, pz, srs, milso, tachi, kyla, supa7 (review-media/<gun>-permutations/all.mp4
in ntre proposals/0057; Milso and Tachi on the bundle build: review-media/<gun>-permutations-bundle/) and the overview reel review-media/research-reload-overview.mp4. Third person, master then
research for each take (fresh dry, partial, resume after the seat, resume after the slap, death mid-reload):
review-media/zr68s-thirdperson-gesture.mp4; spectating bots: review-media/zr68s-spectator-gesture.mp4.

Assets

The models and sound pieces are in NeotokyoRebuild/neoAssets#169 (neoAssets branch research-reload-phases).

Chamber shot and fire during a reload (added 2026-10-16, research only)

A behavior change from master and possibly a balance change, kept on this branch to try it out.

  • The chambered round is shown apart from the magazine: the round above the fire-mode letter is lit while a round is
    chambered and dim when not. After a dry reload the magazine row lights at the seat, and the round lights at the
    charging-handle pull with the magazine one lower. The SRS dims it at a shot until the bolt is home, the Supa7 until
    the pump is home.
  • The trigger ends a partial reload before the magazine drop (the old magazine keeps firing, nothing is gained), or
    after the last step that loads the gun: the magazine tap on guns that have one, else the seat (a2ad518), so it
    skips only steps already done (0.3-0.9 s of tail on a partial reload).
  • With the magazine out, the trigger fires the chambered round: a one-handed shot (fire_chamber_onehand, a short
    flick to center, the shot 0.133 s after the press, 0.200 s on the MPN), then the rest of the reload is a dry reload.
    This part is sv_neo_reload_chamber_shot (default 1, a toggle on purpose so it can be compared and turned off); at
    0 the trigger is ignored while the magazine is out, as before.
  • Removal: set the cvar to 0, or revert "Fire the chambered round while the magazine is out" and "Fire the chambered
    round by default"; the trigger rules come out with "Let the trigger end a reload before the drop or after the seat"
    and the neoAssets commits with the one-handed sequences.
  • Tested: every gun in game, an 8-pin bot census with the cvar at 1 and 3 pins at 0 (no stalls, no cancel loops),
    Debug runs with asserts=0. Not tested: network lag.
  • Open (after 2026-10-30): a resumed reload after a weapon switch should not finish sooner than the full reload minus
    the time already spent before its checkpoint. Measured: 11 resume sequences are early by 0.03-0.63 s when the draw
    is not counted, none when the 0.8 s draw is counted; proposals/0057-reload-cue-timing/timing-rule-partial.md in
    the ntre notes has the table. Not changed yet.

CNEORules::Think() refreshes the ghoster index only after every entity
has thought, so in the frame after the ghost carrier disconnects (a
kick, neo_bot_quota 0) the boundary's Think() still reads the old
index. UTIL_PlayerByIndex() then returns null for the freed edict and
GetFlags() crashes the server. Skip the position update when the
player is gone, the same way GetGhostMarkerPos() already does. The
juggernaut branch reads its index the same way and gets the same check.
CleanUpMap removed each player's neo_predicted_viewmodel_muzzleflash
but kept the neo_predicted_viewmodel it is parented to. A bot with a
pending class change calls RequestSetClass from CNEOBot::Spawn before
CBasePlayer::Spawn recreates the viewmodels, so GiveDefaultItems ->
Weapon_Switch dereferenced the removed muzzle flash viewmodel and the
server crashed (bot_changeclass, then mp_restartgame).

Preserve it like the viewmodel, so the player keeps one for life.
The Tachi and Milso viewmodels carry a node graph from HL2's pistol,
so a reload started from the lowered pose (during a sprint) played
the lowtoidle transition first. DefaultReload took that 0.37-0.40 s
transition as the reload length, and the server refilled the
magazine after about 0.6 s with no reload animation. Skip
transitions for ACT_VM_RELOAD as SetIdealActivity already does for
the draw, so the reload plays at once and lasts its own 2.1 s.
The Tachi and Milso viewmodels have an idle01empty sequence with the
slide held back, which neo never played: an emptied pistol held its
last fire frame for 2.0 s and then idled with the slide forward. The
original NEOTOKYO holds the slide back from the end of the last shot
until the reload. Send ACT_VM_IDLE_EMPTY for ACT_VM_IDLE while the
clip is empty, as the SDK crossbow does, and go to it as the emptying
shot's sequence ends. Models without the sequence are unchanged.
The Tachi and Milso viewmodels ship a drawempty sequence, the normal
draw with the slide held back, but it carries no activity, so an
empty pistol drew with the slide forward and only pulled it back as
the empty idle faded in. Pick drawempty by name in Deploy when the
clip is empty. Models without it, and loaded draws, are unchanged.
A reload credited the magazine only when the whole reload animation
ended, 0.2 to 0.7 s after the last click of the reload sound. Switching
weapons in that window dropped the reload, though the gun sounded done.

Each gun script now names its ReloadReadyTime: the last loud click of
its reload sound, rounded up to the animation frame. Where the charging
handle is home later (Jitte, SRM, MPN, AA13, PZ), the script comment
names that time as the alternative value. The reload completes there
and the rest of the animation plays on. Guns without the key (Supa7)
keep the animation length.
Move the player animation back onto CBasePlayerAnimState, the 2006
SDK base classic NEOTOKYO is built on, which neo compiles but did not
use. CNEOPlayerAnimState replaces CHL2MPPlayerAnimState and takes
CMultiPlayerAnimState out of the build (the files stay on disk).

The base gives classic's torso blend by speed, constant leg ground
speeds, the 3 s face-front timer and its feet rules. The new class
adds the run legs only while sprinting, the jump held until a drawn
landing, one gesture layer for fire, reload, grenade and detpack
events, the lean rule, the NeotokyoRebuild#2108 transitioner reset, and a face-front
timer counted from the last squared frame so server and client agree.
Client-side animation and the network excludes from NeotokyoRebuild#653 are kept.
Keep the end of the reload animation as the time the gun can fire
again, as before this branch, and only load the magazine early: the
owner stays busy until the animation ends, and the weapon loads the
magazine from ItemBusyFrame once the reload's ready time has passed.
A switch after the last click keeps the ammo; firing, aiming and the
sprint lower still wait for the animation to end.

The script key is now ReloadReadyBeforeEnd, the time from the last
reload click to the end of the reload animation, and the ready time is
that much before the end of the reload sequence actually played,
clamped to the reload start. The ready time is networked and predicted
so the client loads the magazine on the same frame.
Four entries, weapon_zr68.reload_{drop,seat,slap,bolt}, for the
per-phase reload pieces sliced from zr68_reload.wav (neoAssets
research-reload-phases 86ebd7f+). CHAN_ITEM, so a later piece from
the same weapon replaces an unfinished one. No code plays them yet.
A weapon script may list, per reload sequence, the phases the
sequence carries (drop, seat, slap, bolt), each with the time it
counts and the sound piece that goes with it, and whether a partial
reload keeps a chambered round. Data only: nothing reads it yet.

The ZR68S gets its block, generated from ntre's segments.csv for the
sequences in neoAssets research-reload-phases 86ebd7f.
A weapon whose script lists ReloadPhases reloads as a linear sequence
of phases instead of one refill at the end. The drop leaves only a
chambered round in the gun (the old magazine is gone), the seat loads
one magazine from the reserve, the bolt chambers a round on a dry
reload. A partial reload of a 30-round gun ends at 31.

The phases credited so far survive a weapon switch; once the weapon is
drawn again the reload resumes and credits only the phases left. Until
the resume sequences are selected, the shipped reload plays and the
earlier phases are skipped. The wait to fire stays the length of the
animation that plays. Weapons without the block (Supa7, SMAC, the
rest for now) take the previous path unchanged.

The ZR68S drops ReloadReadyBeforeEnd: its seat credit replaces it.
The resume sequences' reaches now keep the hand at the shipped reach
pace (neoAssets research-reload-phases b863e63), so each resume
credits its phases 0.5 s later than before.
A reload whose phases name sound pieces plays each piece when its
time in the playing sequence comes (each file starts 0.02 s before
its first transient), instead of the one reload wav at the start.
Phases credited before a switch do not play their pieces again, and a
switch stops the pieces still sounding. Also adds GetMaxRoundsInGun,
the magazine plus a chambered round, for the ammo HUD.
A weapon that keeps a round in the chamber on a partial reload shows
one more bullet slot, so a partial reload of a 30-round magazine reads
31/31. During a dry reload the last slot stays empty until the bolt
chambers a round.
The ZR68S script lists the phases of draw_resume_drop_{dry,partial}
(neoAssets research-reload-phases c61699a), the resume that starts
from the draw after the magazine dropped. A weapon may now list up to
12 reload sequences.
The drop piece now starts with the hand's first soft sounds (neoAssets
research-reload-phases c20c13b), so its first transient is at 0.146 s.
A reload with phases plays the sequence for what is left: the partial
reload when a round is chambered, and after a switch the resume that
starts with the hand reaching back to the next phase (from the drop,
the seat or the slap, dry or partial). After the magazine dropped,
the resume starts in the draw itself, which brings the fresh magazine
up out of view. A model without the sequence plays the shipped reload
and credits only the phases left, as before.
When the bullet row overflows into a + mark, the mark is filled for a
full gun; with the chambered round counted, full is the magazine plus
one.
The ZR68C reloads in phases like the ZR68S (same bones, same sound
pieces), with its sequences from neoAssets research-reload-phases
5edfe2e. Its ReloadReadyBeforeEnd goes: the seat credit replaces it.
The ZR68L reloads in phases with sequences and sound pieces from
neoAssets research-reload-phases d18409d; its own wav's pieces join
the sound script as weapon_zr68l.reload_{drop,seat,slap,bolt}. Its
ReloadReadyBeforeEnd goes: the seat credit replaces it.
Both MX guns reload in phases with the sequences and sound pieces of
neoAssets research-reload-phases 0c24082 (the seat rattle sounds at
the silent magazine seat, 0.27 s before its place in the wav); the
pieces join the sound script as weapon_mx.reload_*. Their
ReloadReadyBeforeEnd goes.
A phase may name sound2 / at2 beside sound / at, for a phase whose
sound comes in two pieces timed apart: the M41's seat has a tap
rattle that belongs at the silent seat and tap clicks that stay on
the tap.
The M41 guns reload in phases with the slap as its own phase, with
the sequences and sound pieces of neoAssets research-reload-phases
3e6d86e. The seat carries two pieces: the tap rattle at the silent
magazine seat and the tap clicks on the tap. Their
ReloadReadyBeforeEnd goes.
A phase's sound piece was only considered until the phase was
credited, so pieces timed after their credit (the M41's slap click,
its tap clicks) never played. Pieces now play on their own times;
those of phases credited before the sequence started, and a partial
reload's bolt, are marked done when the sequence starts.
Both Jitte guns reload in phases with the sequences and sound pieces
of neoAssets research-reload-phases d58ceec. Their ReloadReadyBeforeEnd
goes.
The SRM, SRM-S, MPN and unsilenced MPN reload in phases with the
sequences and sound pieces of neoAssets research-reload-phases f6e2f9e.
Their ReloadReadyBeforeEnd goes.
The AA13 and PZ reload in phases with the sequences and sound pieces
of neoAssets research-reload-phases b5aaa7a; the PZ's drum latch is its
slap phase. Their ReloadReadyBeforeEnd goes.
Phase reloads and draw-resumes set PLAYER_RELOAD, which does nothing
under NEO, so other players saw no reload gesture (the shipped reload
path sends the animation event). Send PLAYERANIMEVENT_RELOAD as the
Supa7 does. A drop or a death now also stops the reload's sound
piece.
A resumed reload played the full reload gesture from the start, and a
partial or interrupted one let it run on past the reload. Send the
gesture's start cycle in the animation event's spare data (the credit
time of the last completed phase in the full reload, as a fraction of
it; 0 for a fresh reload and the draw-resume), and end the gesture
with PLAYERANIMEVENT_RELOAD_END when the reload ends, is holstered or
the weapon is dropped. The animation state starts the layer at that
cycle and resets the slot on the end event. No new networked fields.
The partial and resume sequences were looked up on the weapon entity,
which on the client carries the world model: the lookup failed there,
so the client predicted the shipped reload while the server played the
partial or resume sequence, and the resumed gesture started from 0 on
the owner's own client. Look them up, and time them, on the owner's
viewmodel on both sides.
The table index was sent in 4 signed bits (-8..7), but a gun has up to
12 tables and the ZR68, MX, M41 and PZ guns use index 8 for
draw_resume_drop_partial. The owner's client read another table, so it
predicted the wrong credits and replayed the drop sound piece on a
draw-resume after a partial reload's drop.
The tube's capacity was the clip size minus a live chambered round, so
the gun held 7 in all, as on master and in classic. The tube now holds
the script's clip size (7) and the chamber one more round beside it:
a partial reload ends at 8, a dry reload loads 7 and racks one up
(chamber + 6, 7 in all) with room for a one-shell top-off, and the
standalone rack is offered when the tube holds 7.

GetMaxRoundsInGun becomes virtual so the ammo HUD draws 8 slots for
the Supa7. GetMaxClip1 stays 7 (the tube) for bots and the base code,
as the magazine size does for the other chambered guns.
A dry reload switched away from before its end rack leaves shells in
the tube and the chamber empty. The trigger then fired anyway, and the
shot's own pump fed the tube's top shell, so that shot cost no round
(7 in the gun before and after). The trigger now racks the pump once
(the standalone rack) and the next press fires. Master and classic
fire at once in this state and pump on the next reload; this rack
only ever delays the shot, never brings it earlier.
A chambered slug blocked every shell reload, a rule from master's
single clip count, where the slug was simply the next round to fire.
With the chamber outside the tube, shells now load into the tube
behind a chambered slug and the reload ends without a rack, so a gun
with a slug up still tops off to 8. Loading a second slug onto a
chambered one stays refused, so a slug is never racked out by
accident.
The shot that emptied a Tachi or Milso played its normal fire sequence,
whose slide closes within 0.2 s, and the slide went back only when the
empty idle followed 0.4 s later. Both pistols ship a dry fire sequence
that is a shot's kick with the slide held back throughout, ending on
the empty idle's pose, so the last round's shot plays that instead.
A reload always played the reload sequence, whose slide is forward from
its first frame, so the slide of an empty pistol closed as the reload
began. The Milso ships a reload for an empty gun (ACT_VM_RELOAD_EMPTY)
that keeps the slide back until its release near the end, the same
length as the reload; an empty weapon now plays it where the model has
one.
# Conflicts:
#	src/game/shared/neo/weapons/weapon_neobasecombatweapon.cpp
anim-sdk-base deletes hl2mp_playeranimstate.*, where this branch had
the reload gesture hooks. They move to CNEOPlayerAnimState: the
RELOAD_END event, the resumed reload's start cycle from nData, and the
end of the reload gesture on RELOAD_END.

# Conflicts:
#	src/game/shared/hl2mp/hl2mp_playeranimstate.cpp
#	src/game/shared/hl2mp/hl2mp_playeranimstate.h
A dry Milso or Tachi reload interrupted after the seat has rounds in
the magazine but none chambered, and its slide is still back: the
resume is the slide release alone. The draw and the idle chose their
empty sequences by an empty magazine only, so the slide went forward
on the draw and snapped back when the release resumed.
The NEO anim state ends a gesture at once, so a reload cut short by a
partial magazine's end or a death snapped the third-person gun from
the reload pose to the aim pose in one frame. Fade it out over 0.2 s
as the HL2MP gesture slots did.
A switch mid-reload ended the third-person reload gesture at once, the
same snap from the reload pose to the aim pose that a reload ending
early had. Fade it out over 0.2 s too; a fire or throw gesture still
stops at once on a switch.
The clip counts the chambered round on guns with reload phases. Four
getters split it out for the HUD and the fire rules: whether the gun
has a chamber slot, whether a live round is in it, the rounds in the
magazine and the magazine's capacity. The Supa7 answers from its tube
and chamber.
A gun with a chamber draws its magazine row and, at the row's right
end, one larger glyph for the chamber: lit while a live round is
chambered, in the empty color otherwise. The row's overflow mark now
counts the magazine alone. Number mode shows 29+1/30.
A trigger press during a reload with phases, while a round can fire,
ends the reload where it is and fires on the same tick: before the
old magazine drops it keeps the partly empty magazine, after the
fresh one is seated it keeps what was loaded. A dry reload before its
bolt, and a gun whose magazine is out, ignore the press. Sound pieces
of phases not reached stop; the first shot blends in with fire_blend
where the model has it. One entry point, TryFireDuringReload, called
from ItemBusyFrame; RELOAD_FIRE_INTERRUPTS turns it off.
With sv_neo_reload_chamber_shot on, a trigger press while a partial
reload has the magazine out plays fire_chamber_onehand: the gun
flicks to the hold one-handed, the bullet fires at the table's
fire_at, and the reload goes on dry from the insert. A switch during
the flick cancels the shot. A model without the sequence, the SRS,
and a gun with an empty chamber refuse the shot. The shot keeps the
reload's timers, animation and gesture. The cvar defaults to 0 until
every gun's model has the sequence; the weapon scripts carry the
sequence's phase tables, generated from segments.csv.
@sunzenshen

Copy link
Copy Markdown
Contributor Author

This PR is open for local experimentation, but is probably going to change a lot code wise for initial iterations.

It may be a better to prioritize smaller PRs like #2248 as incremental improvements to the animation workflow.

@sunzenshen sunzenshen added Animations Animation stuff. Including the syncronizing of animations between client and server based on state. Gunplay Gunplay elements in Neotokyo. Grenades, TTK, etc. labels Oct 17, 2026
The large chamber glyph at the row's end crowded the ammo counter.
The fire-mode glyph already carries a small round icon (a bullet, or
a shell on the Supa7) above its letter; it now shows the chamber
instead: the filled color while a round is chambered, the empty color
otherwise, with the letter in the text color as before. The row keeps
counting the magazine alone. Guns without a chamber slot are drawn as
before.
Every gun with a chamber now has its fire_chamber_onehand sequence,
so sv_neo_reload_chamber_shot defaults to 1.
After a shot the SRS chamber holds a spent case until the bolt cycle
brings the next round home, so the chamber icon dims at the shot and
lights when the bolt is home, with the magazine row dropping by one
then instead of at the shot. The moment comes from a "charge" phase
table in the weapon script (the Bolt bone home in the charge
sequence, frame 25). A quick switch or a reload cancels the bolt cycle
as before and the round counts as chambered. Ammo and fire timing are
unchanged.
The shot already racks the pump in the gun's state; the HUD now shows
the chamber dim and the fed round still in the tube until the fire
animation's pump is home, never later than the next shot is allowed.
The pump moments come from the weapon script (ChamberAfterShot 0.600
for a shell, ChamberAfterSlugShot 1.000 for a slug: the Pump bone home
in altfire f18 and fire f30). Ammo and fire timing are unchanged.
On a gun whose reload slaps or taps the magazine home, the trigger
now ends a partial reload only after the slap credit, not at the
seat, so a cancel skips only the steps already complete and stays
close to the full reload time. Guns without a slap still end at the
seat; the rule reads the reload's own phase table.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Animations Animation stuff. Including the syncronizing of animations between client and server based on state. Gunplay Gunplay elements in Neotokyo. Grenades, TTK, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant